package ssm.ann.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.handler.MappedInterceptor;
import org.springframework.web.servlet.mvc.UrlFilenameViewController;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import ssm.ann.config.converter.PropertiesHttpMessageConverter;
import ssm.ann.config.interceptor.GlobalControllerArgumentHandler;
import ssm.ann.config.interceptor.GlobalControllerReturnHandler;
import ssm.ann.config.interceptor.SimpleInterceptor;
import ssm.ann.controller.My_02_AbstractController;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;

@EnableWebMvc // 开启 spring mvc
// @ImportResource(value = {"classpath:/springmvc.xml"})
// 扫描包，只扫描 controller 层，其它 bean 可从父容器(spring)中获取
@ComponentScan(basePackages = {"ssm.ann.controller"}, includeFilters = {
        @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, ControllerAdvice.class})
}, useDefaultFilters = false)
@RequiredArgsConstructor
public class ServletConfig implements WebMvcConfigurer, InitializingBean {
    final ApplicationContext applicationContext;

    @Override
    public void afterPropertiesSet() throws Exception {
        RequestMappingHandlerAdapter requestMappingHandlerAdapter = applicationContext.getBean(RequestMappingHandlerAdapter.class);

        // 这里返回的是一个只读的视图，所以并不能直接操作里面的数据
        List<HandlerMethodArgumentResolver> argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers();
        List<HandlerMethodArgumentResolver> newArgumentResolvers = new ArrayList<>(argumentResolvers);
        newArgumentResolvers.add(new GlobalControllerArgumentHandler());
        requestMappingHandlerAdapter.setArgumentResolvers(newArgumentResolvers);

        List<HandlerMethodReturnValueHandler> returnValueHandlers = requestMappingHandlerAdapter.getReturnValueHandlers();
        List<HandlerMethodReturnValueHandler> newReturnValueHandlers = new ArrayList<>(returnValueHandlers);
        newReturnValueHandlers.add(new GlobalControllerReturnHandler());
        requestMappingHandlerAdapter.setReturnValueHandlers(newReturnValueHandlers);
    }

    /**
     * 该控制器直接跳转到一个页面，该控制器根据请求的 url，解析出视图名，省去了视图名的配置。当然它也可议指定前缀与后缀，如下的配置
     * "/*" 会把它注册为一个默认的 handler
     * 因为这里把它设定为了默认的处理器，所以任何 404 的请求都会到它这里来，交给它处理。例如访问：
     * /democontroller22，因为我配置了前缀后缀，所以最终会到视图 /api/v1/democontroller22.do 里去
     * 它处理请求的方法 handleRequestInternal 都在父类 AbstractUrlViewController 中实现
     */
    // @Bean("/*")
    public UrlFilenameViewController urlFilenameViewController() {
        UrlFilenameViewController controller = new UrlFilenameViewController();
        controller.setPrefix("/api/v1/");
        controller.setSuffix(".do");
        return controller;
    }

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        // configurer.setUseSuffixPatternMatch(false); // 关闭后缀名匹配
        // configurer.setUseTrailingSlashMatch(false); // 关闭最后一个 / 匹配

        // 这样 My_02_AbstractController 上的方法自动就会有此前缀了，而别的 controller 上是不会有的
        // 注意：这是 Spring5.1 后才支持的新特性
        configurer.addPathPrefix("/api/v1", clazz -> clazz.isAssignableFrom(My_02_AbstractController.class));

        // 使用 Spring 提供的 HandlerTypePredicate，更加的强大
        // 若添加了两 prefix 都可以作用在某个 Controller 上，那么会按照放入的顺序（因为它是LinkedHashMap）以先匹配上的为准
        HandlerTypePredicate predicate = HandlerTypePredicate.forBasePackage("spring.mvc.xxx");
        // HandlerTypePredicate predicate = HandlerTypePredicate.forBasePackageClass(My_04_HttpRequestHandler.class);
        // HandlerTypePredicate predicate = HandlerTypePredicate.forAssignableType(...);
        // HandlerTypePredicate predicate = HandlerTypePredicate.forAnnotation(...);
        // HandlerTypePredicate predicate = HandlerTypePredicate.builder()
        //         .basePackage()
        //         .basePackageClass()
        //         .build();
        configurer.addPathPrefix("/api/v2", predicate);
    }

    /**
     * 注册一个默认的 servlet 处理器  让它处理静态资源
     */
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        // 这个 Configurer 只有一个 enable 方法，内部会 new DefaultServletHttpRequestHandler();
        configurer.enable("default"); // 默认值的 servlet 名字就是 default
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) { // 拦截器
        registry.addInterceptor(new SimpleInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/admin/abc");
    }

    /**
     * 如果说上述方式是交给 Spring 去自动处理，这种方式相当于自己手动来处理
     * 请务必注意：此处的返回值必须是 MappedInterceptor，而不能是 HandlerInterceptor 否则不生效
     * 因为 BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), MappedInterceptor.class, true, false)
     * 这个方法它只去找 MappedInterceptor 类型，如果你是父类型，那就匹配不上了，这和工厂方法的 Bean 定义信息有关
     */
    // @Bean
    public MappedInterceptor myHandlerInterceptor() {
        String[] includePatterns = {"/api/v1/hello"};
        return new MappedInterceptor(includePatterns, new SimpleInterceptor());
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 支持 Ant 风格的路径匹配
        registry.addResourceHandler("/resource/**").addResourceLocations("/WEB-INF/static/");
    }

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // 注册异步的拦截器、默认的超时时间、任务处理器 TaskExecutor 等
        // configurer.registerCallableInterceptors();
        // configurer.registerDeferredResultInterceptors();
        // configurer.setDefaultTimeout();
        // configurer.setTaskExecutor(mvcTaskExecutor());
    }

    public ThreadPoolTaskExecutor mvcTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setQueueCapacity(100);
        executor.setMaxPoolSize(25);
        return executor;
    }

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.forEach(c -> {
            Class<AbstractJackson2HttpMessageConverter> jacksonClazz = AbstractJackson2HttpMessageConverter.class;
            if (jacksonClazz.isAssignableFrom(c.getClass())) {
                AbstractJackson2HttpMessageConverter jacksonConverter = jacksonClazz.cast(c);
                // 定制 HttpMessageConverter 消息转换器的 ObjectMapper 实例
                ObjectMapper objectMapper = jacksonConverter.getObjectMapper();
                objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
                objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
            }
        });

        // 因为此转换器职责已经足够单一，所以放在首位是没有问题的
        // 若放在末尾，将可能不会生效（比如如果 Fastjson 转换器处理所有的类型的话，所以放在首位最为保险）
        converters.add(0, new PropertiesHttpMessageConverter());
    }

    // 不要复写这个方法，否则最终只有这里配置的转换器，SpringMVC 默认的就不会加载了
    // @Override
    // public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    // }

    @Bean
    public ObjectMapper objectMapper() {
        return Jackson2ObjectMapperBuilder.json()
                .dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))
                // 设置 ApplicationContext 和 Spring 完成深度整合
                .applicationContext(applicationContext)
                .moduleClassLoader(applicationContext.getClassLoader())
                .build();
    }
}
